/* Copyright(C) 2006-2008 Dave Sexton  *
 * http://www.codeplex.com/aip         *
 * http://www.codeplex.com/aip/license *
 ***************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using DaveSexton.Web.Controls;
using DaveSexton.Web.Controls.Resources;

namespace DaveSexton.Web.Controls
{
	/// <summary>
	/// Specifies sets of characters that may be sampled at random by the <see cref="RandomString"/> class.
	/// </summary>
	/// <remarks>
	/// Multiple values can be or'ed together to specify larger character sets.
	/// </remarks>
	[Flags]
	public enum CharacterSets
	{
		/// <summary>
		/// Lowercase letters.
		/// </summary>
		LowercaseLetters = 1,
		/// <summary>
		/// Uppercase letters.
		/// </summary>
		UppercaseLetters = 2,
		/// <summary>
		/// All letters, regardless of case.
		/// </summary>
		Letters = LowercaseLetters | UppercaseLetters,
		/// <summary>
		/// The number zero (0).
		/// </summary>
		Zero = 4,
		/// <summary>
		/// The positive integers from 1 to 9, inclusive.
		/// </summary>
		PositiveDigits = 8,
		/// <summary>
		/// The positive integers up to 9, including zero (0).
		/// </summary>
		PositiveDigitsAndZero = Zero | PositiveDigits,
		/// <summary>
		/// All letters, regardless of case, and positive integers up to 9, including zero (0).
		/// </summary>
		LettersAndPositiveDigits = Letters | PositiveDigits,
		/// <summary>
		/// Symbols in the ASCII range starting from the left curly brace { to the right curly brace }.
		/// </summary>
		Symbols = 16,
		/// <summary>
		/// Includes all available character sets.
		/// </summary>
		All = Letters | PositiveDigitsAndZero | Symbols
	}

	/// <summary>
	/// Generates randomized strings of characters and basic English words.
	/// </summary>
	/// <seealso cref="Resources.Dictionaries"/>
	/// <seealso cref="BasicEnglishAutoInputProtectionTextProvider"/>
	/// <seealso cref="RandomCharactersAutoInputProtectionTextProvider"/>
	/// <seealso cref="RandomIntervals"/>
	/// <threadsafety static="true"/>
	public static class RandomString
	{
		#region Private
		private static readonly Dictionary<CharacterSets, Point> ranges = InitializeRanges();
		private static Dictionary<int, List<string>> ogdenBasicEnglishDictionary;
		private static int ogdenBasicEnglishDictionaryLongestWordLength;
		#endregion

		#region Methods
		/// <summary>
		/// Initializes the internal dictionary of character ranges that may be sampled at random.
		/// </summary>
		/// <remarks>
		/// A <see cref="Point"/> structure is used to store the numeric equivalents of the starting and ending characters of each range.
		/// <see cref="Point.X"/> is the starting character and <see cref="Point.Y"/> is the ending character.  A range can be defined as 
		/// the character equivalents of all numeric values from the starting value to the ending value, inclusive.
		/// </remarks>
		/// <returns><see cref="Dictionary{TKey,TValue}"/> of <see cref="CharacterSets"/> as keys and <see cref="Point"/> structures as values.</returns>
		private static Dictionary<CharacterSets, Point> InitializeRanges()
		{
			Dictionary<CharacterSets, Point> ranges = new Dictionary<CharacterSets, Point>();
			ranges[CharacterSets.LowercaseLetters] = new Point((int) 'a', (int) 'z');
			ranges[CharacterSets.UppercaseLetters] = new Point((int) 'A', (int) 'Z');
			ranges[CharacterSets.Zero] = new Point((int) '0', (int) '0');
			ranges[CharacterSets.PositiveDigits] = new Point((int) '1', (int) '9');
			ranges[CharacterSets.Symbols] = new Point((int) '{', (int) '}');
			return ranges;
		}

		/// <summary>
		/// Ensures that the words from the Ogden Basic English dictionary are indexed in memory by their lengths.
		/// </summary>
		/// <seealso cref="Resources.Dictionaries.OgdenBasicEnglishAlphabetical"/>
		/// <seealso cref="CreateBasicEnglishWord"/>
		[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.Synchronized)]
		private static void EnsureOgdenBasicEnglishDictionary()
		{
			if (ogdenBasicEnglishDictionary != null)
				return;

			ogdenBasicEnglishDictionary = new Dictionary<int, List<string>>(16);

			foreach (string word in Resources.Dictionaries.OgdenBasicEnglishAlphabetical.Split(','))
			{
				int length = word.Length;
				List<string> words;

				if (!ogdenBasicEnglishDictionary.ContainsKey(length))
					ogdenBasicEnglishDictionary.Add(word.Length, words = new List<string>(64));
				else
					words = ogdenBasicEnglishDictionary[length];

				words.Add(word);

				if (ogdenBasicEnglishDictionaryLongestWordLength < length)
					ogdenBasicEnglishDictionaryLongestWordLength = length;
			}
		}

		/// <summary>
		/// Returns a word at random, from the Ogden Basic English dictionary, with a length that is between the 
		/// specified <paramref name="minLength"/> and <paramref name="maxLength"/> values, inclusive.
		/// </summary>
		/// <seealso cref="EnsureOgdenBasicEnglishDictionary"/>
		/// <seealso cref="BasicEnglishAutoInputProtectionTextProvider"/>
		/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="minLength"/> is less than zero or 
		/// <paramref name="maxLength"/> is less than <paramref name="minLength"/>.</exception>
		/// <param name="minLength">The minimum amount of letters that a word may have.</param>
		/// <param name="maxLength">The maximum amount of letters that a word may have.</param>
		/// <returns>A random basic English word of a length that is between the specified <paramref name="minLength"/> and 
		/// <paramref name="maxLength"/> values.</returns>
		public static string CreateBasicEnglishWord(int minLength, int maxLength)
		{
			if (minLength < 0)
				throw new ArgumentOutOfRangeException("minLength", minLength, Errors.PositiveIntOrZeroRequired);

			if (maxLength < minLength)
				throw new ArgumentOutOfRangeException("maxLength", maxLength,
					string.Format(System.Globalization.CultureInfo.CurrentUICulture, Errors.RandomString_MaxLessThanMin, minLength));

			Random rnd = new Random();

			int length = rnd.Next(minLength, maxLength + 1);

			if (length == 0)
				return string.Empty;

			EnsureOgdenBasicEnglishDictionary();

			List<string> words = ogdenBasicEnglishDictionary[Math.Min(length, ogdenBasicEnglishDictionaryLongestWordLength)];

			return words[rnd.Next(0, words.Count)];
		}

		/// <summary>
		/// Returns a string of random characters from the specified set of <paramref name="charactersAllowed"/>, excluding 
		/// the characters in <paramref name="charactersNotAllowed"/>, with a length that is between the 
		/// specified <paramref name="minLength"/> and <paramref name="maxLength"/> values, inclusive.
		/// </summary>
		/// <remarks>
		/// <para>
		/// A case-sensitive comparison is used to compare the characters to the characters in <paramref name="charactersNotAllowed"/>.
		/// </para>
		/// <para>
		/// If <paramref name="minLength"/> is zero then an empty string may be returned.
		/// </para>
		/// </remarks>
		/// <seealso cref="RandomCharactersAutoInputProtectionTextProvider"/>
		/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="minLength"/> is less than zero or 
		/// <paramref name="maxLength"/> is less than <paramref name="minLength"/>.</exception>
		/// <param name="minLength">The minimum amount of characters that the string may have.</param>
		/// <param name="maxLength">The maximum amount of characters that the string may have.</param>
		/// <param name="charactersAllowed">Specifies the characters that may not appear in the string.</param>
		/// <param name="charactersNotAllowed">Specifies the character sets from which characters may be chosen at random.</param>
		/// <returns>A string of random characters.</returns>
		public static string Create(int minLength, int maxLength, CharacterSets charactersAllowed, string charactersNotAllowed)
		{
			if (minLength < 0)
				throw new ArgumentOutOfRangeException("minLength", minLength, Errors.PositiveIntOrZeroRequired);

			if (maxLength < minLength)
				throw new ArgumentOutOfRangeException("maxLength", maxLength,
					string.Format(System.Globalization.CultureInfo.CurrentUICulture, Errors.RandomString_MaxLessThanMin, minLength));

			List<int> validCharSetValues = new List<int>((int[]) Enum.GetValues(typeof(CharacterSets)));

			for (int i = 0; i < validCharSetValues.Count; )
			// for each index in the array of CharacterSet constant values remove 
			// the ones that aren't bit-aligned or aren't selected in charSets
			{
				int csVal = validCharSetValues[i];

				if (!IsBitAligned(csVal) || (csVal & (int) charactersAllowed) == 0)
				{
					validCharSetValues.RemoveAt(i);
					continue;
				}

				i++;
			}

			Random rnd = new Random();

			int length = rnd.Next(minLength, maxLength + 1);

			if (length == 0)
				return string.Empty;

			StringBuilder value = new StringBuilder(length);

			if (!string.IsNullOrEmpty(charactersNotAllowed))
			{
				for (int i = 0; i < length; i++)
				// for each index in the string value generate a random character
				{
					CharacterSets validCharacters = (CharacterSets) validCharSetValues[rnd.Next(0, validCharSetValues.Count)];

					Point range = ranges[validCharacters];

					char c = (char) rnd.Next(range.X, range.Y + 1);

					if (charactersNotAllowed.IndexOf(c) == -1)		// case-sensitive comparison
						value.Append(c);
					else
						// the current character must be replaced because it has been excluded
						i--;
				}
			}
			else
			{
				for (int i = 0; i < length; i++)
				// for each index in the string value generate a random character
				{
					// randomly choose a set of characters and get the character range for that set
					Point range = ranges[(CharacterSets) validCharSetValues[rnd.Next(0, validCharSetValues.Count)]];

					value.Append((char) rnd.Next(range.X, range.Y + 1));
				}
			}

			return value.ToString();
		}

		/// <summary>
		/// Determines whether the specified <paramref name="value"/> represents a number that exists on a binary boundary; 
		/// e.g., 1, 2, 4, 8, 16, 32, 64, etc.
		/// </summary>
		/// <param name="value">The number to check.</param>
		/// <returns><see langword="true">True</see> if the specified <paramref name="value"/> is bit-aligned; otherwise, 
		/// <see langword="false"/>.</returns>
		private static bool IsBitAligned(int value)
		{
			// WARN: not checking for value <= 0

			// must convert value to float so that division by 2 isn't rounded
			float fval = (float) value;

			do
			{
				if (fval == 1)
					return true;
			}
			while ((fval /= 2) >= 1);

			return false;
		}
		#endregion
	}
}
